package pl.edu.mimuw.irs.core.model.analysis.dd;

import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.Map.Entry;

import pl.edu.mimuw.irs.core.global.IDManager;
import pl.edu.mimuw.irs.core.model.analysis.RequirementPlace;
import pl.edu.mimuw.irs.core.model.analysis.ResourcePlace;
import pl.edu.mimuw.irs.core.model.analysis.TPair;
import pl.edu.mimuw.irs.core.model.design.IRSActivity;
import pl.edu.mimuw.irs.core.model.design.processing.Requirement;
import pl.edu.mimuw.irs.core.model.design.processing.Resource;
import pl.edu.mimuw.irs.core.model.design.processing.Role;
import pl.edu.mimuw.irs.core.model.petrinet.Transition;


public class DDHire extends Transition {

	private static final long serialVersionUID = -6106666703435041826L;

	// *** Attributes

	private ActivityWrapper activity;
	private DependenciesGraph graph;
	private Map<Resource, TPair> hiringMap;
	private LinkedHashMap<Requirement, Set<ResourcePlace>> requirementsResourcesMap;
	private Map<IRSActivity, Set<ResourcePlace>> hiredResources;
	public ActivityWrapper getActivity() {
		return activity;
	}
	public DependenciesGraph getGraph() {
		return graph;
	}
	public Map<Resource, TPair> getHiringMap() {
		if (hiringMap == null) hiringMap = 
			new LinkedHashMap<Resource, TPair>();
		return hiringMap;
	}
	public LinkedHashMap<Requirement, Set<ResourcePlace>> getRequirementsResourcesMap() {
		if (requirementsResourcesMap == null) requirementsResourcesMap = 
			new LinkedHashMap<Requirement, Set<ResourcePlace>>();
		return requirementsResourcesMap;
	}
	public Map<IRSActivity, Set<ResourcePlace>> getHiredResources() {
		if (hiredResources == null) hiredResources = 
			new LinkedHashMap<IRSActivity, Set<ResourcePlace>>();
		return hiredResources;
	}
	
	// *** Constructors
	
	public DDHire(IDManager idManager, ActivityWrapper activity, DependenciesGraph graph,
			Map<Resource, TPair> hiringMap,
			LinkedHashMap<Requirement, Set<ResourcePlace>> requirementsResourcesMap, 
			Map<IRSActivity, Set<ResourcePlace>> map) {
		super(idManager, idManager.getPrefixedNewId("Hire" + activity.getActivity().getId()), 
				activity.getActivity().getPosition().left());
		this.activity = activity;
		this.graph = graph;
		this.hiringMap = hiringMap;
		this.requirementsResourcesMap = requirementsResourcesMap;
		this.hiredResources = map;
	}
	
	// *** Methods

	/*
	 * Reimplemented invoke of simple transition. Here it contains full
	 * Deadlock Detection before actual execution. 
	 */
	@Override
	public void invoke() {
		/*
		 * Get some set of workers capable of doing this activity.
		 */
		Map<RequirementPlace, Set<ResourcePlace>> map = getWorkers();
		if (map == null) return;
		/* We have to convert the above map to more usable figure */
		Set<ResourcePlace> resources = new LinkedHashSet<ResourcePlace>();
		for (Entry<RequirementPlace, Set<ResourcePlace>> entry : map.entrySet()) {
			Set<ResourcePlace> resourcesPlaces = entry.getValue();
			resources.addAll(resourcesPlaces);
		}
		/* 
		 * Having proper figure we can now process the result. Before we ask 
		 * if the activity is deadlocking we have to reserve all the resources. 
		 * So we do it now. If activity is deadlocking we will free them at once.
		 */
		reserveResources(resources);
		
		if (graph.isDeadlocking(activity)) {
			freeResources(resources);
		} else {
			/*
			 * All is well - we can start activity. We notify graph about it.
			 */
			graph.start(activity, map.keySet());
			super.invoke();
			Map<IRSActivity, Set<ResourcePlace>> hiredMap = getHiredResources();
			hiredMap.put(getActivity().getActivity(), resources);
		}
	}
	
	private Map<RequirementPlace, Set<ResourcePlace>> getWorkers() {
		/*
		 * Getting workers consists of two parts - first gathers resources required
		 * to execute the activity (possibly too many). Second frees resources which are
		 * not required to run this activity.
		 */
		
		Map<Requirement, Integer> required = 
			getActivity().getActivity().getRequirement().getRequirementsMap();
		Map<Requirement, Integer> matchedSum = 
			new HashMap<Requirement, Integer>();
		Map<RequirementPlace, Set<ResourcePlace>> matched = 
			new HashMap<RequirementPlace, Set<ResourcePlace>>();
		boolean result;
		
		/*
		 * FIRST PART - USING DERIVED RESOURCES
		 */
		result = coverWithDerived(required, matchedSum, matched);
		if (result) return matched;
		
		/*
		 * SECOND PART - GATHERING RESOURCES.
		 */
		result = coverWithNew(required, matchedSum, matched);
		
		return matched;
	}
	
	private boolean coverWithDerived(Map<Requirement, Integer> required,
			Map<Requirement, Integer> matchedSum,
			Map<RequirementPlace, Set<ResourcePlace>> matched) {
		/*
		 * TODO : Get unused roles of derived resources to cover requirements
		 * for this activity.
		 */
		Set<ResourcePlace> derivedSet = getActivity().getDerivedResources();
		Map<ResourcePlace, Map<Requirement, Integer>> map =
			new LinkedHashMap<ResourcePlace, Map<Requirement, Integer>>();
		
		for (ResourcePlace resource : derivedSet) {
			map.put(resource, resource.getMetRequirements2(required));
		}
		
		return false;
	}
	
	private boolean coverWithNew(Map<Requirement, Integer> required,
			Map<Requirement, Integer> matchedSum,
			Map<RequirementPlace, Set<ResourcePlace>> matched) {
		
		Set<ResourcePlace> usedResources = new HashSet<ResourcePlace>();
		
		for (Entry<Requirement, Integer> entry : required.entrySet()) {
			/*
			 * If this requirement has already been matched - we should skip
			 * it.
			 */
			Integer sum = matchedSum.get(entry.getKey());
			if (!(sum == null) && sum >= entry.getValue()) continue;
			
			Set<ResourcePlace> resources = getRequirementsResourcesMap().get(entry.getKey());
			/*
			 * If nobody is capable of doing this requirement we must end. 
			 */
			if (resources == null) return false;
			/*
			 * If resource is available we can use it to cover this requirement.
			 */
			boolean covered = false;
			List<ResourcePlace> shuffled = new ArrayList<ResourcePlace>(resources);
			Collections.shuffle(shuffled);
			/* 
			 * Every randomly chosen resource is checked if it is available or
			 * if it has not been processed already. 
			 */
			for (ResourcePlace resourcePlace : shuffled) {
				if (usedResources.contains(resourcePlace) ||
						resourcePlace.getTokens() == 0) continue;
				else {
					/* 
					 * Because this resource is available, we can increment all 
					 * requirements met by this resource (including the one we are
					 * searching for).
					 */
					for (Entry<RequirementPlace, Integer> requirementEntry : 
							resourcePlace.getMetRequirements().entrySet()) {
						Requirement requirement = requirementEntry.getKey().getRequirement();
						Set<ResourcePlace> set = matched.get(requirementEntry.getKey());
						if (set == null) {
							set = new LinkedHashSet<ResourcePlace>();
							matched.put(requirementEntry.getKey(), set);
						}
						set.add(resourcePlace);
						Integer sumValue = matchedSum.get(requirement);
						if (sumValue == null) sumValue = 0;
						matchedSum.put(requirement, sumValue + requirementEntry.getValue());
					}
					
					usedResources.add(resourcePlace);
					
					/*
					 * If searched requirement is covered, we can break the loop and
					 * move on to the next requirement.
					 */
					if (matchedSum.get(entry.getKey()) >= entry.getValue()) {
						covered = true;
						break;
					}
				}
			}
			
			/*
			 * If any requirement cannot be met by any available resource, then
			 * we cannot start this activity.
			 */
			if (!covered) {
				return false;
			}
		}
		
		/*
		 * Here we have all requirements met. But possibly there are some resources, which
		 * can be not required. For example if we have two requirements - A and B, and we
		 * have two resources A and AB, then we can firstly hire A for the A requirement
		 * and then AB for B requirement. However AB could do both A and B by itself and
		 * so A is useless here. So we have to look through matches to see if we can find
		 * any such resource and fire it.
		 */
		Set<Resource> mandatory = new HashSet<Resource>();
		/* 
		 * First we have to get all mandatory resources. So we will be able to easily
		 * check which is mandatory and which is not.
		 */
		
		for (Entry<RequirementPlace, Set<ResourcePlace>> entry : matched.entrySet()) {
			Requirement requirement = entry.getKey().getRequirement();
			/*
			 * The difference between how many requirements are needed and how many 
			 * are met, defines which resources are mandatory.
			 */
			Integer requiredCount = required.get(requirement);
			if (requiredCount == null) continue;
			int availableCount = matchedSum.get(requirement);

			addIfMandatory(mandatory, entry.getKey(), 
					availableCount, requiredCount, entry.getValue());
		}
		
		/*
		 * Knowing which resources are mandatory we know which are not as well.
		 * Because above set contains ALL mandatory resources, so we can fire any
		 * of the rest and requirements still will be met. But during every such
		 * fire we have to update the contents of mandatory set, because it can grow.
		 */
		List<ResourcePlace> nonMandatory = new LinkedList<ResourcePlace>();
		for (ResourcePlace resourcePlace : usedResources) {
			if (!mandatory.contains(resourcePlace.getResource())) {
				nonMandatory.add(resourcePlace);
			}
		}
		Collections.shuffle(nonMandatory);
		/*
		 * Here we iterate over non-mandatory resources.
		 */
		for (ResourcePlace resource : nonMandatory) {
			if (mandatory.contains(resource.getResource())) continue;
			/*
			 * Let us refuse this resource, and update mandatory set.
			 */
			/*
			 * XXX : Efficiency?
			 */
			usedResources.remove(resource);
			for (Entry<RequirementPlace, Integer> entry : resource.getMetRequirements().entrySet()) {
				Requirement requirement = entry.getKey().getRequirement();
				Set<ResourcePlace> resources = matched.get(entry.getKey());
				/* 
				 * If for this requirement this is the only resource we can
				 * just remove whole list.
				 */
				if (resources.size() < 2) matched.remove(entry.getKey());
				else {
					resources.remove(resource);
					Integer requiredCount = required.get(requirement);
					if (requiredCount == null) continue;
					int availableCount = matchedSum.get(requirement);
					availableCount = availableCount - entry.getValue();
					matchedSum.put(requirement, availableCount);
					/*
					 * Now refresh mandatory set.
					 */
					addIfMandatory(mandatory, entry.getKey(), 
							availableCount, requiredCount, resources);
				}
			}
		}
		
		return true;
	}
	
	private void addIfMandatory(Set<Resource> mandatory, 
			RequirementPlace requirement,
			int availableCount,
			int requiredCount,
			Set<ResourcePlace> resources) {
		int diff = availableCount - requiredCount;
		for (ResourcePlace resource : resources) {
			if (mandatory.contains(resource)) continue;
			/*
			 * If resource provides more than diff requirements, then it
			 * is mandatory, so we add it. We also mark which roles of this
			 * resource are used to match this requirement. 
			 */
			if (resource.getMetRequirements().get(requirement) > diff) {
				for (Entry<Role, Integer> entry : requirement.getRequirement().getRolesMap().entrySet()) {
					Integer available = resource.getRolesAvailable().get(entry.getKey());
					resource.getRolesAvailable().put(entry.getKey(), 
							available - entry.getValue());
				}
				if (!mandatory.contains(resource)) mandatory.add(resource.getResource());
			}
		}
	}
	
	private void reserveResources(Set<ResourcePlace> resources) {
		Map<Resource, TPair> hiringMap = getHiringMap();
		for (ResourcePlace resourcePlace : resources) {
			TPair pair = hiringMap.get(resourcePlace.getResource());
			pair.getHire().invoke();
		}
	}
	
	private void freeResources(Set<ResourcePlace> resources) {
		Map<Resource, TPair> hiringMap = getHiringMap();
		for (ResourcePlace resourcePlace : resources) {
			TPair pair = hiringMap.get(resourcePlace.getResource());
			pair.getFire().invoke();
		}
	}
	
	@Override
	public String toString() {
		return getId();
	}
}
